Tule je en dober link o grafih:
http://www-math.cudenver.edu/~wcherowi/courses/m4408/m4408.html  Tam je tudi
nekaj ilustracij, s katerimi bo tale mail zaradi svoje plain-text narave pac
neizogibno bolj skop. :)

Graf je matematicna struktura, ki jo sestavljajo tocke (vertices; ednina je
"vertex") in povezave (edges).  Tockam vcasih pravijo tudi vozlisca (nodes).
Tocke pogosto oznacujemo s crkami s konca abecede, npr. u, v, w.

Povezava je lahko usmerjena (directed) (v tem primeru je povezava od u do v
nekaj drugega kot povezava od v do u in v grafu je lahko npr. ena prisotna,
druga pa ne, ali pa sta prisotni obe (ali pa nobena)) ali pa neusmerjena
(undirected) (v tem primeru je povezava med u in v ista stvar kot povezava
med v in u).

Obicajno je tako, da so v grafu ali vse povezave usmerjene (in mu recemo
"usmerjen graf") ali pa so vse povezave neusmerjene (in mu recemo
"neusmerjen graf").

Kadar grafe risemo, obicajno predstavimo vsako tocko z majhnim krozcem,
povezave pa s crtami (ce so neusmerjene) ali puscicami (ce so usmerjene) med
tockami.

Grafi so koristni pri mnogih problemih.  Obicajno s tockami predstavimo neke
reci, s povezavami pa neke odnose ali zveze med temi recmi -- lahko bi torej
rekli, da so koristne informacije pri grafu shranjene predvsem v povezavah.
Najbolj ocitni primeri so verjetno razna komunikacijska ali infrastrukturna
omrezja -- lahko bi na primer cestno omrezje predstavili z grafom: za vsako
mesto bi bila po ena tocka, dve tocki pa bi bili povezani, ce bi obstajala
cesta, ki bi vodila naravnost od enega mesta do drugega.  Vcasih ima vsaka
povezava se kaksne dodatne podatke ali lastnosti, npr. dolzino, kapaciteto,
ceno ali kaj podobnega (koliko km je dolga cesta od kraja A do kraja B?).

Dobro pri grafih je, da lahko mnoge probleme prevedemo v probleme na grafih,
ce nek vidik problema predstavimo z grafom.  Za probleme na grafih pa je
znanih ze veliko algoritmov, s katerimi se jih lahko lotimo.  V tem smislu
je torej graf koristen pripomocek za abstrakcijo.

Se nekaj terminologije v zvezi z grafi.  V usmerjenem grafu lahko recemo, ce
obstaja povezava od u do v, da u "kaze na" v.  Potem so vse tocke, ki kazejo
na v, njeni "predhodniki", vse, na katere kaze v, pa so njeni "nasledniki".
Predhodnikom in naslednikom recemo z eno besedo "sosedje".  Tudi v
neusmerjenem grafu lahko recemo, da sta si u in v sosedi, ce obstaja med
njima povezava.  Za povezavo od u do v (ali pa med u in v, ce je
neusmerjena) recemo, da je "incidencna" na tocki u in v.  Stevilo sosedov
neke tocke je "stopnja" (degree) ali "valenca" te tocke.  Stevilo
predhodnikov je "vhodna stopnja", stevilo naslednikov je "izhodna stopnja"
(in-degree, out-degree).

Povezava bi naceloma lahko povezovala tudi tocko samo s sabo -- taksni
pravimo "zanka".  Vendar pa najveckrat delamo z grafi, kjer zank ni oz. nima
smisla, da bi bile.

Mnozico tock pogosto oznacujemo z V, mnozico povezav pa z E.  Neusmerjeno
povezavo med u in v lahko oznacimo npr. z (u:v), usmerjeno od u do v pa z
(u, v) ali (u->v).  Nekateri uporabljajo n za stevilo tock, m pa za stevilo
povezav, vendar bom jaz raje uporabljal kar |V| in |E|, ker si drugace tezko
zapomnim, kaj je m in kaj je n.  Grafu, ki ima n tock, pogosto recemo, da je
to graf "na n tockah".

Ce imamo neusmerjen graf in zaporedje tock u[0], u[1], ..., u[n] in obstaja
povezava med u[i-1] in u[i] (za vsak i od 1 do n), pravimo, da je to
"sprehod" po nasem grafu.  Dolzina sprehoda je stevilo povezav na njem -- v
tem primeru torej n.  Ce imamo usmerjen graf, zahtevamo obicajno to, da
morajo obstajati povezave od u[i-1] do u[i] (sprehod mora spostovati smer
povezav).  Ce se v sprehodu nobena tocka ne pojavi po veckrat, pa mu pravimo
"pot".  (Nekateri pa z besedo pot mislijo vsak sprehod, temu, cemur sem jaz
tule rekel pot, pa bi oni rekli "preprosta pot" (ali veriga ali kaj
podobnega).)  Ce bi pri sprehodu zahtevali se, naj se zacne in konca pri
isti tocki, bi mu rekli obhod, ce pa se za povrhu se nobena tocka vmes ne bi
pojavila po veckrat, bi mu rekli cikel.

Neusmerjen graf, v katerem se da iz vsake tocke priti do vsake druge (po
kaksni poti), je "povezan" (connected).  Drugace pa je sestavljen iz vec
kosov, od katerih je vsak zase povezan -- tem kosom pravimo "povezane
komponente".  Podobne pojme lahko uvedemo tudi pri usmerjenih grafih, kjer
recemo, da sta dve tocki "krepko povezani", ce obstaja pot od ene do druge
in tudi pot nazaj od druge do prve; in "sibko povezani", ce se da priti od
ene do druge v primeru, ce ignoriramo smer povezav.  Potem lahko govorimo o
krepko oz. sibko povezanih komponentah grafa, torej skupinah tock, v katerih
je vsak par tock krepko oz. sibko povezan.

Grafu, v katerem ni nobenega cikla, pravimo "aciklicen".  Aciklicen
neusmerjen graf je "gozd", ce pa je za povrhu se povezan, je "drevo".  Ta
dva izraza verjetno potrebujeta nekaj razlage.  Narisite si nek neusmerjen
povezan graf brez ciklov; mislite si, da je narejen iz vrvic, pa zagrabite
za katero koli tocko in jo dvignite, tako da potem preostanek grafa (tocke
in povezave) visi dol s tocke, ki jo drzite; nato pa si mislite, da je ves
graf v hipu cisto otrdel (npr. ni vec iz vrvic, pac pa iz zice), pa ga lahko
zdaj zapicite v zemljo in to s tisto tocko naprej, ki ste jo prej dvignili v
zrak.  Zdaj bo stvar podobna, ce ze ne pravemu drevesu, pa vsaj nekaksnemu
grmu.  (Je pa res, da v racunalnistvu drevesa obicajno risemo tako, da niso
zapicena v tla, pac pa v strop in potem ostale tocke in povezave visijo
dol.)  No, ce pa imamo neusmerjen aciklicen graf, ki ni povezan, bi pa lahko
isto naredili z vsako od njegovih povezanih komponent in tako iz vsake
dobili eno drevo -- tak graf je torej skupek dreves, zato mu tudi recemo
"gozd".  Za vajo se lahko bralec preprica, da ima drevo na n tockah vedno
natancno n-1 povezav; in tudi v splosnem ima povezan neusmerjen graf na n
tockah vsaj n-1 povezav.

Dostikrat imamo opravka tudi z usmerjenimi aciklicnimi grafi -- takim
pravijo pogosto kar "dagi", kar pride iz angleske kratice za directed
acyclic graph.  V takem grafu torej ne sme biti ciklov, vendar ne pozabimo,
da stejejo le taki cikli, ki spostujejo smer povezav.  Torej graf s tockami
a, b, c, d in povezavami a->b, a->c, b->d, c->d je dag, ker a-b-d-c ali kaj
podobnega v njem ni cikel.

Grafu, ki vsebuje vse mozne povezave, pravimo "poln graf" ali "klika" (full
graph, clique).  No, pogosto vendarle recemo klika tudi takemu grafu, ki ne
vsebuje zank, pac pa le vse mozne povezave med dvema razlicnima tockama.  O
kliki bi lahko govorili tako pri usmerjenih kot pri neusmerjenih grafih,
vendar se mi zdi, da veckrat mislijo na neusmerjen graf.

Ce od nekega grafa G obdrzimo le nekaj tock in nekatere povezave, ki
potekajo le med temi tockami, pravimo, da je dobljeni graf "podgraf"
prvotnega grafa G.

--

Matematiki seveda lahko recejo G = (V, E), kako pa naj graf predstavimo
racunalnikarji v svojih programih?  Obicajna sta dva pristopa: z matriko
sosednosti in s seznami sosedov.

Pri matriki sosednosti (adjacency matrix) si mislimo, da imamo tocke
ostevilcene od 1 do n.  Potem ustanovimo "matriko" -- dvodimenzionalno
tabelo, npr.

  var Adj: array[1..n, 1..n] of Boolean;

v kateri element Adj[u, v] pove, ce obstaja v grafu povezava od u do v.  Ce
imamo neusmerjen graf, bo ta matrika najbrz simetricna -- v Adj[v, u] bo
vedno enaka vrednost kot v Adj[u, v].  To slednje je po svoje potratno -- ce
bi bili res v skripcih, bi lahko poskusili zadevo zmanjsati tako, da bi
hranili le polovico (npr. spodnji trikotnik) te matrike, tako da bi bila Adj
v bistvu tabela kazalcev na razlicno dolge tabele -- Adj[u] bi kazal na
tabelo dolzine u, ki bi vsebovala podatke Adj[u, v] za 1 <= v <= u.  A to je
ze ekstremizem, ki se ga obicajno ni treba lotevati.

Lepo pri tej predstavitvi je, da je za konkreten par tock zelo enostavno
ugotoviti, ce obstaja med njima povezava -- pogledamo v Adj[u, v], pa je.
Ce imajo povezave se kaksne dodatne lastnosti, npr. dolzine, jih lahko tudi
hranimo v Adj[u, v] (ti elementi zdaj ne bi bili vec tipa boolean, pac pa
integer ali kaj podobnega).  Ce smo skrti, lahko Adj namesto tabele
booleanov zasnujemo kot bitno karto in tako vsakemu elementu namenimo le en
bit.  Vsaka vrstica te tabele bi bila dolga na primer Ceil(n/8) bajtov
[spomnimo se enega od prejsnjih mailov -- Ceil je zaokrozanje navzgor -- v
praksi bi clovek to najbrz naredil kot (n + 7) div 8), v-ti element pa bi
nasli kot (v mod 8)-i bit (v div 8)-ega bajta te vrstice.

Ce nase tocke niso stevila od 1 do n (ali od 0 do n-1 ali kaj podobnega), bi
si lahko pomagali s kaksno hash tabelo, da bi jih poceni preslikali v taksne
stevilske oznake.

Slabo pri tej predstavitvi pa je, da ima pri grafu na n tockah ta tabela
zdaj kar n^2 elementov, kar lahko hitro postane zelo potratno -- sploh v
primeru, ce ima ta graf veliko manj kot n^2 povezav, saj ima v tem primeru
vecina elementov vrednost False in precej nekoristno zrejo prostor.  (Grafu,
ki ima veliko povezav, pravimo, da je "gost" (dense), ce jih ima malo, pa,
da je "redek" (sparse).  To sta taksni bolj megleni in manj formalni oznaki,
lahko pa si ju predstavljamo takole: ce je stevilo povezav nekako v istem
redu velikosti kod n^2, je graf gost, ce pa je bolj v takem redu velikosti
kot n, pa je graf redek.)

Slabo je tudi to, da moramo pri delu z grafi pogosto nekaj narediti z vsemi
predhodniki ali vsemi nasledniki ali vsemi sosedami neke tocke -- v tem
primeru nam ne kaze drugega, kot da gremo po celi vrstici ali stolpcu nase
matrike, taka vrstica ali stolpec pa ima vedno n elementov, ne glede na to,
koliko je dejansko sosedov.  Ce je graf redek in imajo tocke malo sosedov,
bomo veliko casa nekoristno porabili za pregledovanje samih vrednosti False
v nasi tabeli.

Matrika sosednosti je torej neugodna zato, ker nic ne izkoristi redkosti
grafa -- tudi ce ima graf malo povezav, bo matrika sosednosti zrla prav
toliko pomnilnika in s preiskovanjem sosedov bomo imeli prav toliko dela,
kot ce bi bil graf gost.  Ce pa vemo, da je graf kolikor toliko gost (ali pa
dovolj majhen, da nas neucinkovitost ne moti, tudi ce je graf redek), je
matrika sosednosti fina stvar, ker je preprosta in ker je poceni priti do
podatka o obstoju ali neobstoju neke konkretne povezave.

--

Drugi nacin za predstavitev grafov v pomnilniku pa so seznami sosedov.  V
tem primeru imamo za vsako tocko en seznam (linked list), v katerem so
shranjeni njeni sosedje.  No, ce je graf usmerjen, je smiselno hraniti za
vsako tocko po dva seznama -- enega s predhodniki in enega z nasledniki.
Zdaj vsako povezavo predstavljata dva elementa v nasih seznamih -- povezavo
u->v na primer predstavimo tako, da povemo, da je v u-jev naslednik in u
v-jev predhodnik.  Podobno bi povezavo u:v predstavili tako, da bi povedali,
da je v u-jev sosed, u pa v-jev sosed.

Dobro pri tej predstavitvi je, da je kolicina pomnilnika, ki ga porabi,
sorazmerna s stevilom povezav -- ce je povezav manj, imajo nasi seznami manj
elementov in zasedajo manj pomnilnika.  Tudi ce se moramo sprehoditi po vseh
sosedih (ali predhodnikih ali naslednikih), je treba iti le skozi ustrezni
seznam -- tam bomo dobili natancno vse tiste tocke, ki so res sosedje, in
nobenih drugih.  To je sploh lepo, ce je graf redek, saj je v tem primeru to
velika izboljsava v primerjavi z matriko sosednosti.

Slabo pa je, da je tezje ugotoviti, ce obstaja v grafu neka konkretna
povezava u:v (ipd. za u->v) -- moramo se sprehoditi po celem seznamu u-jevih
(ali v-jevih) sosedov in preveriti, ce je v njem tudi tocka v (ali u).  To
je se zlasti slabo, ce je graf gost in so ti seznami dolgi.  Vendar pa se
lahko tej slabosti vsaj deloma izognemo, ce vse povezave dodamo tudi v hash
tabelo -- kot kljuce porabimo na primer kar pare (u, v).  Potem lahko
prakticno v konstantnem casu preverimo, ce dolocena povezava obstaja ali ne.
[Vcasih se lahko tej slabosti izognemo tudi tako, da operacije "preveri, ce
obstaja povezava u:v" sploh ne potrebujemo. :)]

Se ena slabost predstavitve s seznami sosedov je, da zahteva malo vec
knjigovodstva kot matrika sosednosti -- zdaj se moramo ukvarjati s seznami,
alocirati elemente seznamov, zonglirati s kazalci in tako naprej.  Tudi
kazalci pozrejo nekaj pomnilnika.  Vendar pa prednosti obicajno vec kot
odtehtajo te slabosti -- sploh ce ima graf veliko tock, kajti takrat si
pogosto preprosto ne moremo privosciti matrike n*n.  (Pri resevanju nalog je
pogosto tudi tako, da graf na zacetku zgradimo, potem pa ga ne spreminjamo
vec, ne dodajamo ali brisemo ipd., zato je tudi s knjigovodstvom nekaj manj
dela.)

Slabost je mogoce tudi ta, da je zdaj podatek o povezavi u:v shranjen na
dveh ali treh koncih -- v seznamu u-jevih sosedov je navedena tocka v, v
seznamu v-jevih sosedov je navedena tocka u, za povrhu pa imamo mogoce se v
hash tabeli kljuc (u, v).  Ce hocemo o povezavi hraniti se kaksne dodatne
podatke (npr. dolzino), se moramo odlociti, kje jih bomo hranili, da bodo
samo na enem mestu in da bodo vedno pri roki (no, ce jih ne bomo nic
spreminjali, so lahko tudi na vec mestih, ce seveda niso preveliki -- ce pa
jih bomo spreminjali, bi bile pri podvajanju podatkov tezave z
zagotavljanjem konsistentnosti (usklajenosti) med razlicnimi kopijami).
Lahko bi take podatke hranili v hash tabeli, lahko pa celo isto celico (isti
zapis v pomnilniku) povezemo hkrati v seznam u-jevih sosedov, seznam v-jevih
sosedov in se v hash tabelo.  Tam imamo potem namesto enega kazalca na
naslednji element seznama kar tri take kazalce.  Lahko pa bi tudi celica v
enem seznamu (tista, ki pove, da je u v-jev sosed) kazala na celico v drugem
seznamu (tisto, ki pove, da je v u-jev sosed) -- nekaj takega sem uporabil
npr. v svoji resitvi naloge s Pruferjevim kodom
(http://www2.arnes.si/~sudjbran/rtk/Rtk2002.pdf).

Pri usmerjenem grafu se lahko tudi zgodi, da seznamov predhodnikov ne bomo
potrebovali, pac pa le sezname naslednikov (ali pa obratno).  Tedaj pac ni
treba hraniti tistega, cesar ne potrebujemo.

Vcasih seznamov sosedov tudi ni treba hraniti kot sezname, ampak jih lahko v
kaksni tabeli -- to se posebej velja v primeru, ce lahko graf na samem
zacetku lepo zgradimo in ga kasneje nic vec ne spreminjamo.  V tem primeru
bi imeli lahko tabele:

  var Sosedje: array[1..2*nPovezav] of Integer;
      Stopnja, PrviSosed: array[1..nTock] of Integer;

In bi bili sosedje tocke u tocke Sosedje[PrviSosed[u]], Sosedje[PrviSosed[u]
+ 1], ..., Sosedje[PrviSosed[u] + Stopnja[u] - 1].  Nekaj podobnega bi lahko
naredili tudi za usmerjen graf, kjer bi posebej hranili predhodnike in
posebej naslednike.

--

Skratka, pri predstavitvi grafov v pomnilniku se je pametno na zacetku
vprasati, s kaksnimi grafi bomo imeli opravka in kaksne operacije bomo nad
njimi izvajali.  Potem lahko izberemo taksno razlicico predstavitve, ki nam
bo najbolj ustrezala.  Vidimo, da imamo veliko moznosti za lovljenje
ravnotezja med casovno zahtevnostjo, porabo pomnilnika in delom z
implementacijo.

No, se ena moznost na temo predstavitve grafov je ta, da grafa sploh ne
predstavimo eksplicitno.  Vcasih obstaja kaksno preprosto pravilo, s katerim
lahko za dve tocki preverimo, ce sta sosedi ali ne, ali pa kaksno pravilo, s
katerim lahko nastejemo vse sosede neke tocke.  Na primer: ce so tocke pari
(x, y) in ima vsak (x, y) le stiri sosede: (x-1, y), (x+1, y), (x, y-1) in
(x, y+1).  Tak graf je torej nekaksna karirasta mreza (takemu pogosto
pravijo "grid" ali "mesh").  Ker je njegova struktura cisto predvidljiva,
pravzaprav ni nujno, da podatke o strukturi grafa hranimo na katerega od
zgoraj opisanih nacinov.

--

Z grafi se lahko lotimo tudi naloge o opravilih in projektih, ki sem jo
poslal naokoli prejsnji teden, pa tudi nekaj naslednjih nalog bo povezanih z
grafi.

LP, Janez